<?php
/**
 * World Weather Online
 *
 * @copyright  Copyright (c) 2011 Celebrio Software
 * @package    WeatherApi
 */
namespace Celebrio\WeatherApi\WorldWeatherOnline;

use \SimpleXMLIterator;
use \InvalidArgumentException;

use Nette\Object;
use Nette\Templates\TemplateHelpers;

use Celebrio\WeatherApi\WorldWeatherOnline\CurrentConditions;
use Celebrio\WeatherApi\WorldWeatherOnline\ForecastConditions;
use Celebrio\WeatherApi\WorldWeatherOnline\AreaInformations;
use Celebrio\WeatherApi\WorldWeatherOnline\LocationNotFoundException;
use Celebrio\Util\RequestBuilder;
use Celebrio\WeatherApi\WorldWeatherOnline\InvalidAPIKeyException;
use Celebrio\WeatherApi\WorldWeatherOnline\WWOException;
use Celebrio\Util\DateParser;
use \Celebrio\Caching\CacheName;

class WorldWeatherOnline extends Object{
    
    const QUERY_BASIC_LINK_SEARCH_WEATHER = "http://free.worldweatheronline.com/feed/weather.ashx?";
    const QUERY_BASIC_LINK_SEARCH_CITY = "http://www.worldweatheronline.com/feed/search.ashx?";
    const WEATHER_CACHE_EXPIRATION_TIME = 30;  // in minutes
    const CITY_CACHE_EXPIRATION_TIME = 48; // in hours
    
    public static $api_key;
    private $place;
    private $latitude;
    private $longitude;
    private $requestLink = "";
    private $format;
    private $numOfDays = 5;
    private $suportedFormats = array("xml", "json"); //TODO: csv can be supported too, it is supported by the wwo server


    public function __construct($latitude = null, $longitude = null, $place = null) {
    	$this->clearQuery();
    	$this->setPosition($latitude, $longitude);
    	$this->setPlace($place);
    	$this->format = "xml";
    }

    public function getForecast() {
    	$this->composeQuery();
    	$response = $this->makeRequest();
    	$data = $this->performOutput($response);
    	return $this->composeArray($data);
    }

    public function getCityRecords($cityName) {
    	$this->composeQueryForCitySearch($cityName);
    	return $this->makeRequest(60 * self::CITY_CACHE_EXPIRATION_TIME);

    }

    
    
    
    
    /**
     * Make request based on url. When searching
     * for the first time, the result is fetched and cached.
     * The next time, the cache contents is returned.
     * @param $cacheTime - expiration time of cache in minutes
     * @throws ServerNotRespondingException
     * @return url response
     */
    private function makeRequest($cacheTime = self::WEATHER_CACHE_EXPIRATION_TIME) {
    	$cache = CacheName::generate($this);

        $url = $this->requestLink;
  
       	$cacheName = "wwo_" . hash('sha1',$url);

    	if (!$cache->offsetExists($cacheName)) {
        	$ch = curl_init($url);
        	curl_setopt($ch, CURLOPT_HEADER, 0);
        	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

        	$response = curl_exec($ch);
        	$info = curl_getinfo($ch);
        	
        	if($info["http_code"] == 500) {
        		\Logger::getRootLogger()->error("Server is not responding on request: " . $url);
            	throw new ServerNotRespondingException(_("Server is not responding"));
        	}
        	$cache->save($cacheName, $response, array('expire' => time() + 60 * $cacheTime));
        	
    	} else {
        	$response = $cache->offsetGet($cacheName);
        }
        
        
        return $response;
    }
    
    private function composeQueryForCitySearch($cityName) {
        $cityName = urlencode($cityName);
        $this->clearQuery(self::QUERY_BASIC_LINK_SEARCH_CITY);
        $this->append("query=" . $cityName);
        $this->append("num_of_results=3");
        $this->specifyFormat("json");
        $this->specifyPrivateKey();
    }
    
    private function composeQuery() {
        $this->clearQuery();
        $this->specifyPlace();
        $this->specifyPrivateKey();
        $this->specifyNumOfDays($this->numOfDays);
        $this->enableLocalObsTime();
        $this->specifyFormat("xml");
    }
    
    private function performOutput($response) {
        if($this->format === "xml") {
            return $this->performOutputXML($response);
        } elseif($this->format === "json") {
            return $this->performOutputJSON($response);
        }
    }
    
    
    private function performOutputJSON($document) {
        $decoded = json_decode($document);
        $data = $decoded->data;
        $this->composeCurrentConditionsJSON($data->current_condition);
        
    }
    
    private function performOutputXML($document) {
        $document = new \SimpleXMLIterator($document);
        $error = $document->xpath("/data/error");
        if(!empty($error)) {
            $this->handleError($error[0]);
        }
        $output = array();
        
        $current = $document->xpath("/data/current_condition");        
        
        if(!empty($current)) {
            $currentConditions = $this->composeCurrentConditions($current[0]);
            $output["currentConditions"] = $currentConditions;
        }
        
        $weather = $document->xpath("/data/weather");
        
        if(!empty($weather)) {
            $forecast = array();
            foreach($weather as $conditions) {
                $composedForecast = $this->composeForecast($conditions);
                $forecast[] = $composedForecast;
            }
            $output["forecast"] = $forecast;
        }
        
        $locationData = $document->xpath("/data/nearest_area");
        if(!empty($locationData)) {
            $info = $this->composeAreaInformations($locationData[0]);
            $output["areaInfo"] = $info;
        }
        return $output;
        
    }
    
    private function composeAreaInformations(SimpleXMLIterator $locationData) {
        $informations = new AreaInformations();
        
        foreach($locationData as $info) {
            $informations->setAttribute($info->getName(), trim($info->__toString()));
        }
        return $informations;
    }
    
    private function composeCurrentConditions(SimpleXMLIterator $currentWeather) {
        $current = new CurrentConditions();
        $current->setCloudcover($currentWeather->cloudcover->__toString());
        $current->setHumidity($currentWeather->humidity->__toString());
        $current->setObservation_time(date("G:i"));
        $current->setPrecipMM($currentWeather->precipMM->__toString());
        $current->setPressure($currentWeather->pressure->__toString());
        $current->setTemp_C($currentWeather->temp_C->__toString());
        $current->setTemp_F($currentWeather->temp_F->__toString());
        $current->setVisibility($currentWeather->visibility->__toString());
        $current->setWeatherCode($currentWeather->weatherCode->__toString());
        $current->setWeatherDesc($currentWeather->weatherDesc->__toString());
        $current->setWeatherIconUrl($currentWeather->weatherIconUrl->__toString());
        $current->setWinddir16Point($currentWeather->winddir16Point->__toString());
        $current->setWinddirDegree($currentWeather->winddirDegree->__toString());
        $current->setWindspeedKmph($currentWeather->windspeedKmph->__toString());
        $current->setWindspeedMiles($currentWeather->windspeedMiles->__toString());
        return $current;
    }
    
    private function composeForecast(SimpleXMLIterator $weather) {
        $forecast = new ForecastConditions();
        $forecast->setDate($weather->date->__toString());
        $forecast->setDateParsed($weather->date->__toString());
        $forecast->setPrecipMM($weather->precipMM->__toString());
        $forecast->setTempMaxC($weather->tempMaxC->__toString());
        $forecast->setTempMaxF($weather->tempMaxF->__toString());
        $forecast->setTempMinC($weather->tempMinC->__toString());
        $forecast->setTempMinF($weather->tempMaxF->__toString());
        $forecast->setWeatherCode($weather->weatherCode->__toString());
        $forecast->setWeatherDesc($weather->weatherDesc->__toString());
        $forecast->setWeatherIconUrl($weather->weatherIconUrl->__toString());
        $forecast->setWinddir16Point($weather->winddir16Point->__toString());
        $forecast->setWinddirDegree($weather->winddirDegree->__toString());
        $forecast->setWindspeedKmph($weather->windspeedKmph->__toString());
        $forecast->setWindspeedMiles($weather->windspeedMiles->__toString());
        $forecast->setWinddirection($weather->winddirection->__toString());
        $forecast->setAverageTemps();
        return $forecast;
    }
    
    private function composeJSON($data) {
        return json_encode($this->makeArray($data));
    }
    
    private function composeArray($data) {
        return $this->makeArray($data);
    }
    
    private function makeArray($data) {
        $current = $data["currentConditions"];
        $properties = array();
        $properties["precipMM"] = $current->getPrecipMM();
        $properties["weatherCode"] = $current->getWeatherCode();
        $properties["weatherDesc"] = $current->getWeatherDesc();
        $properties["weatherIconUrl"] = $current->getWeatherIconUrl();
        $properties["winddir16Point"] = $current->getWinddir16Point();
        $properties["winddirDegree"] = $current->getWinddirDegree();
        $properties["windspeedKmph"] = $current->getWindspeedKmph();
        $properties["windSpeedMiles"] = $current->getWindspeedMiles();
        $properties["cloudcover"] = $current->getCloudcover();
        $properties["humidity"] = $current->getHumidity();
        $properties["observation_time"] = $current->getObservation_time();
        $properties["pressure"] = $current->getPressure();
        $properties["temp_C"] = $current->getTemp_C();
        $properties["temp_F"] = $current->getTemp_F();
        $properties["visibility"] = $current->getVisibility();
        $properties["windDirNat"] = $current->getWindDirNat();
        $complete["current"] = $properties;
        //$properties = array($properties); 
        
        
        $forecastArray = array();
        foreach($data["forecast"] as $forecast) {
            $properties = array();
            $properties["dayOfWeek"] = $forecast->getDayOfWeek();
            $properties["precipMM"] = $forecast->getPrecipMM();
            $properties["weatherCode"] = $forecast->getWeatherCode();
            $properties["weatherDesc"] = $forecast->getWeatherDesc();
            $properties["weatherIconUrl"] = $forecast->getWeatherIconUrl();
            $properties["winddir16Point"] = $forecast->getWinddir16Point();
            $properties["winddirDegree"] = $forecast->getWinddirDegree();
            $properties["windspeedKmph"] = $forecast->getWindspeedKmph();
            $properties["windSpeedMiles"] = $forecast->getWindspeedMiles();
            $properties["date"] = $forecast->getDate();
            $properties["dateParsed"] = $forecast->getDateParsed();
            $properties["tempAverageC"] = $forecast->getTempAverageC();
            $properties["tempAverageF"] = $forecast->getTempAverageF();
            $properties["tempMaxC"] = $forecast->getTempMaxC();
            $properties["tempMaxF"] = $forecast->getTempMaxF();
            $properties["tempMinC"] = $forecast->getTempMinC();
            $properties["tempMinF"] = $forecast->getTempMinF();
            $properties["winddirection"] = $forecast->getWinddirection();
            $properties["windDirNat"] = $forecast->getWindDirNat();
            
            $forecastArray[] = $properties;
        }
        $complete["weather"] = $forecastArray;
        $complete["titles_of_days"] = array(
            "now" => _("Now"),
            "today" => _("Today"),
            "tomorrow" => _("Tomorrow")
        );
        $complete["new_cache"] = true;
        $complete = array("data" => $complete);
        return $complete;
    }

    private function specifyPlace() {
        if(($this->latitude == null && $this->longitude == null) && $this->place == null) {
            throw new \InvalidStateException("There are not enaugh parameters to determine location");
        }
        if(empty($this->place)) {
            $this->append("q=" . $this->latitude . "," . $this->longitude);
        } else {
            $this->append("q=" . $this->place);
        }
    }
    
    private function specifyNumOfDays($numOfDays = "2") {
        $this->append("num_of_days=" . $numOfDays);
    }
    
    private function enableLocalObsTime() {
        $this->append("extra=" . "localObsTime");
    }
    
    private function specifyPrivateKey($privateKey = null) {
        if(empty($privateKey)) {
            $privateKey = self::$api_key;
        }
        $this->append("key=" . $privateKey);
    }
    
    //TODO : Implement
    private function specifyDate($date = null) {
        throw new \NotImplementedException("This functionality will be available soon");
    }
    
    /**
     * Can be "xml", "json", "csv". Default is xml
     * At this time only available and supported is xml from our side
     * 
     * @param type $format in which data will be returned
     */
    private function specifyFormat($format = null) {
        if(empty($this->format)) {
            $this->format = "xml";
        }
        if(in_array($format, $this->suportedFormats)) {
            $this->format = $format;
            $this->append("format=" . $this->format);
        } else {
            throw new InvalidArgumentException("Invalid output format.");
        }
    }
    
    /**
     * Enable include of location of nearest weather point. Default is no
     */
    private function enableLocation() {
        $this->append("includeLocation=yes");
    }
    
    /**
     * Disable comments. Default is yes.
     */
    private function disableComments() {
        $this->append("show_comments=no");
    }
    
    private function disableNormalOutput() {
        $this->append("fx=no");
    }
    
    private function  disableCurrentConditionsOutput() {
        $this->append("fx=no");
    }


    private function handleError($error) {
        $errorType = $error->msg->__toString();
        $wrongAccout = strpos($errorType, "User account does not exist");
        
        if($wrongAccout !== false) {
            throw new InvalidAPIKeyException(_("API key is not valid or does not exists."));
        } else {
            throw new WWOException("Something went wrong: " . $errorType);
        }
    }

    private function append($appendString) {
        //if last character is not & or ? we add & to the end of string
        if(!in_array(substr($this->requestLink, -1, 1), array("&", "?"))) {
            $this->requestLink .= "&";
        }
        $this->requestLink .= $appendString;
    }
    
    public function setPosition($latitude, $longitude) {
        $this->latitude = $latitude;
        $this->longitude = $longitude;
    }


    public function setPlace($place) {
        $this->place = $place;
    }
    
    public function setFormat($format) {
        if(!in_array($format, array("xml", "json", "csv"))) {
            throw new \InvalidArgumentException("This format ($format) is not supported.");
        } else {
            $this->format = $format;
        }
    }
    
    private function clearQuery($resetPoint = null) {
        if($resetPoint == null) {
            $resetPoint = self::QUERY_BASIC_LINK_SEARCH_WEATHER;
        }
        $this->requestLink = $resetPoint;
    }
}